#include "../git-compat-util.h"
#include "../git-curl-compat.h"
#ifndef WIN32
#include <dlfcn.h>
#endif

/*
 * The ABI version of libcurl is encoded in its shared libraries' file names.
 * This ABI version has not changed since October 2006 and is unlikely to be
 * changed in the future. See https://curl.se/libcurl/abi.html for details.
 */
#define LIBCURL_ABI_VERSION "4"

typedef void (*func_t)(void);

#ifndef WIN32
#ifdef __APPLE__
#define LIBCURL_FILE_NAME(base) base "." LIBCURL_ABI_VERSION ".dylib"
#else
#define LIBCURL_FILE_NAME(base) base ".so." LIBCURL_ABI_VERSION
#endif

/**
 * Loads the shared library with the given name.
 * Returns a handle to the loaded library.
 */
static void *load_library(const char *name)
{
	return dlopen(name, RTLD_LAZY);
}

/**
 * Loads the function with the given name from the loaded library handle.
 * Returns a function pointer to the loaded function.
 */
static func_t load_function(void *handle, const char *name)
{
	/*
	 * Casting the return value of `dlsym()` to a function pointer is
	 * explicitly allowed in recent POSIX standards, but GCC complains
	 * about this in pedantic mode nevertheless. For more about this issue,
	 * see https://stackoverflow.com/q/31526876/1860823 and
	 * http://stackoverflow.com/a/36385690/1905491.
	 */
	func_t f;
	*(void **)&f = dlsym(handle, name);
	return f;
}
#else
#define LIBCURL_FILE_NAME(base) base "-" LIBCURL_ABI_VERSION ".dll"

/**
 * Loads the shared library with the given name.
 * Returns a handle to the loaded library.
 */
static void *load_library(const char *name)
{
	size_t name_size = strlen(name) + 1;
	const char *path = getenv("PATH");
	char dll_path[MAX_PATH];

	while (path && *path) {
		const char *sep = strchrnul(path, ';');
		size_t len = sep - path;

		if (len && len + name_size < sizeof(dll_path)) {
			memcpy(dll_path, path, len);
			dll_path[len] = '/';
			memcpy(dll_path + len + 1, name, name_size);

			if (!access(dll_path, R_OK)) {
				wchar_t wpath[MAX_PATH];
				int wlen = MultiByteToWideChar(CP_UTF8, 0, dll_path, -1, wpath, ARRAY_SIZE(wpath));
				void *res = wlen ? (void *)LoadLibraryExW(wpath, NULL, 0) : NULL;
				if (!res) {
					DWORD err = GetLastError();
					char buf[1024];

					if (!FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM |
							    FORMAT_MESSAGE_ARGUMENT_ARRAY |
							    FORMAT_MESSAGE_IGNORE_INSERTS,
							    NULL, err, LANG_NEUTRAL,
							    buf, sizeof(buf) - 1, NULL))
						xsnprintf(buf, sizeof(buf), "last error: %ld", err);
					error("LoadLibraryExW() failed with: %s", buf);
				}
				return res;
			}
		}

		path = *sep ? sep + 1 : NULL;
	}

	return NULL;
}

/**
 * Loads the function with the given name from the shared library handle.
 * Returns a function pointer to the loaded function.
 */
static func_t load_function(void *handle, const char *name)
{
	return (func_t)GetProcAddress((HANDLE)handle, name);
}
#endif

typedef char *(*curl_easy_escape_type)(CURL *handle, const char *string, int length);
static curl_easy_escape_type curl_easy_escape_func;

typedef void (*curl_free_type)(void *p);
static curl_free_type curl_free_func;

typedef CURLcode (*curl_global_init_type)(long flags);
static curl_global_init_type curl_global_init_func;

typedef CURLsslset (*curl_global_sslset_type)(curl_sslbackend id, const char *name, const curl_ssl_backend ***avail);
static curl_global_sslset_type curl_global_sslset_func;

typedef void (*curl_global_cleanup_type)(void);
static curl_global_cleanup_type curl_global_cleanup_func;

typedef struct curl_slist *(*curl_slist_append_type)(struct curl_slist *list, const char *data);
static curl_slist_append_type curl_slist_append_func;

typedef void (*curl_slist_free_all_type)(struct curl_slist *list);
static curl_slist_free_all_type curl_slist_free_all_func;

typedef const char *(*curl_easy_strerror_type)(CURLcode error);
static curl_easy_strerror_type curl_easy_strerror_func;

typedef CURLM *(*curl_multi_init_type)(void);
static curl_multi_init_type curl_multi_init_func;

typedef CURLMcode (*curl_multi_add_handle_type)(CURLM *multi_handle, CURL *curl_handle);
static curl_multi_add_handle_type curl_multi_add_handle_func;

typedef CURLMcode (*curl_multi_remove_handle_type)(CURLM *multi_handle, CURL *curl_handle);
static curl_multi_remove_handle_type curl_multi_remove_handle_func;

typedef CURLMcode (*curl_multi_fdset_type)(CURLM *multi_handle, fd_set *read_fd_set, fd_set *write_fd_set, fd_set *exc_fd_set, int *max_fd);
static curl_multi_fdset_type curl_multi_fdset_func;

typedef CURLMcode (*curl_multi_perform_type)(CURLM *multi_handle, int *running_handles);
static curl_multi_perform_type curl_multi_perform_func;

typedef CURLMcode (*curl_multi_cleanup_type)(CURLM *multi_handle);
static curl_multi_cleanup_type curl_multi_cleanup_func;

typedef CURLMsg *(*curl_multi_info_read_type)(CURLM *multi_handle, int *msgs_in_queue);
static curl_multi_info_read_type curl_multi_info_read_func;

typedef const char *(*curl_multi_strerror_type)(CURLMcode error);
static curl_multi_strerror_type curl_multi_strerror_func;

typedef CURLMcode (*curl_multi_timeout_type)(CURLM *multi_handle, long *milliseconds);
static curl_multi_timeout_type curl_multi_timeout_func;

typedef CURL *(*curl_easy_init_type)(void);
static curl_easy_init_type curl_easy_init_func;

typedef CURLcode (*curl_easy_perform_type)(CURL *curl);
static curl_easy_perform_type curl_easy_perform_func;

typedef void (*curl_easy_cleanup_type)(CURL *curl);
static curl_easy_cleanup_type curl_easy_cleanup_func;

typedef CURL *(*curl_easy_duphandle_type)(CURL *curl);
static curl_easy_duphandle_type curl_easy_duphandle_func;

typedef CURLcode (*curl_easy_getinfo_long_type)(CURL *curl, CURLINFO info, long *value);
static curl_easy_getinfo_long_type curl_easy_getinfo_long_func;

typedef CURLcode (*curl_easy_getinfo_pointer_type)(CURL *curl, CURLINFO info, void **value);
static curl_easy_getinfo_pointer_type curl_easy_getinfo_pointer_func;

typedef CURLcode (*curl_easy_getinfo_off_t_type)(CURL *curl, CURLINFO info, curl_off_t *value);
static curl_easy_getinfo_off_t_type curl_easy_getinfo_off_t_func;

typedef CURLcode (*curl_easy_setopt_long_type)(CURL *curl, CURLoption opt, long value);
static curl_easy_setopt_long_type curl_easy_setopt_long_func;

typedef CURLcode (*curl_easy_setopt_pointer_type)(CURL *curl, CURLoption opt, void *value);
static curl_easy_setopt_pointer_type curl_easy_setopt_pointer_func;

typedef CURLcode (*curl_easy_setopt_off_t_type)(CURL *curl, CURLoption opt, curl_off_t value);
static curl_easy_setopt_off_t_type curl_easy_setopt_off_t_func;

static char ssl_backend[64];

/**
 * Lazily loads the libcurl library and resolves its symbols.
 * This avoids needing to link libcurl into all git binaries
 * when only some need it. It is called when curl functionality
 * is first needed.
 */
static void lazy_load_curl(void)
{
	static int initialized;
	void *libcurl = NULL;
	func_t curl_easy_getinfo_func, curl_easy_setopt_func;

	if (initialized)
		return;

	initialized = 1;
	if (ssl_backend[0]) {
		char dll_name[64 + 16];
		snprintf(dll_name, sizeof(dll_name) - 1,
			 LIBCURL_FILE_NAME("libcurl-%s"), ssl_backend);
		libcurl = load_library(dll_name);
	}
	if (!libcurl)
		libcurl = load_library(LIBCURL_FILE_NAME("libcurl"));
	if (!libcurl)
		die("failed to load library '%s'", LIBCURL_FILE_NAME("libcurl"));

	curl_easy_escape_func = (curl_easy_escape_type)load_function(libcurl, "curl_easy_escape");
	curl_free_func = (curl_free_type)load_function(libcurl, "curl_free");
	curl_global_init_func = (curl_global_init_type)load_function(libcurl, "curl_global_init");
	curl_global_sslset_func = (curl_global_sslset_type)load_function(libcurl, "curl_global_sslset");
	curl_global_cleanup_func = (curl_global_cleanup_type)load_function(libcurl, "curl_global_cleanup");
	curl_slist_append_func = (curl_slist_append_type)load_function(libcurl, "curl_slist_append");
	curl_slist_free_all_func = (curl_slist_free_all_type)load_function(libcurl, "curl_slist_free_all");
	curl_easy_strerror_func = (curl_easy_strerror_type)load_function(libcurl, "curl_easy_strerror");
	curl_multi_init_func = (curl_multi_init_type)load_function(libcurl, "curl_multi_init");
	curl_multi_add_handle_func = (curl_multi_add_handle_type)load_function(libcurl, "curl_multi_add_handle");
	curl_multi_remove_handle_func = (curl_multi_remove_handle_type)load_function(libcurl, "curl_multi_remove_handle");
	curl_multi_fdset_func = (curl_multi_fdset_type)load_function(libcurl, "curl_multi_fdset");
	curl_multi_perform_func = (curl_multi_perform_type)load_function(libcurl, "curl_multi_perform");
	curl_multi_cleanup_func = (curl_multi_cleanup_type)load_function(libcurl, "curl_multi_cleanup");
	curl_multi_info_read_func = (curl_multi_info_read_type)load_function(libcurl, "curl_multi_info_read");
	curl_multi_strerror_func = (curl_multi_strerror_type)load_function(libcurl, "curl_multi_strerror");
	curl_multi_timeout_func = (curl_multi_timeout_type)load_function(libcurl, "curl_multi_timeout");
	curl_easy_init_func = (curl_easy_init_type)load_function(libcurl, "curl_easy_init");
	curl_easy_perform_func = (curl_easy_perform_type)load_function(libcurl, "curl_easy_perform");
	curl_easy_cleanup_func = (curl_easy_cleanup_type)load_function(libcurl, "curl_easy_cleanup");
	curl_easy_duphandle_func = (curl_easy_duphandle_type)load_function(libcurl, "curl_easy_duphandle");

	curl_easy_getinfo_func = load_function(libcurl, "curl_easy_getinfo");
	curl_easy_getinfo_long_func = (curl_easy_getinfo_long_type)curl_easy_getinfo_func;
	curl_easy_getinfo_pointer_func = (curl_easy_getinfo_pointer_type)curl_easy_getinfo_func;
	curl_easy_getinfo_off_t_func = (curl_easy_getinfo_off_t_type)curl_easy_getinfo_func;

	curl_easy_setopt_func = load_function(libcurl, "curl_easy_setopt");
	curl_easy_setopt_long_func = (curl_easy_setopt_long_type)curl_easy_setopt_func;
	curl_easy_setopt_pointer_func = (curl_easy_setopt_pointer_type)curl_easy_setopt_func;
	curl_easy_setopt_off_t_func = (curl_easy_setopt_off_t_type)curl_easy_setopt_func;
}

/**
 * URL-encodes a string.
 *
 * This escapes the given input string so it is suitable for use in a URL.
 *
 * @param handle cURL handle
 * @param string String to be URL-encoded
 * @param length Length of string in bytes
 * @return Escaped string that must be freed by caller
 */
char *curl_easy_escape(CURL *handle, const char *string, int length)
{
	lazy_load_curl();
	return curl_easy_escape_func(handle, string, length);
}

/**
 * Free memory returned from libcurl
 *
 * This frees memory that was allocated and returned from a libcurl function.
 * Any memory returned by libcurl must be freed after use with this function.
 *
 * @param p Pointer to memory to free
 */
void curl_free(void *p)
{
	lazy_load_curl();
	curl_free_func(p);
}

/**
 * Initialize the cURL library
 *
 * This initializes global state for the cURL library like SSL engines,
 * DNS caching, etc. It must be called before any cURL handles are created.
 *
 * @param flags Bitmask of global initialization options. See CURL_GLOBAL_*
 * @return CURLcode return code
 */
CURLcode curl_global_init(long flags)
{
	lazy_load_curl();
	return curl_global_init_func(flags);
}

/**
 * Set the global default SSL backend
 *
 * This sets the global default backend to use for SSL connections. It can be
 * overridden on a per easy handle basis with curl_easy_setopt.
 *
 * @param id backend id as provided in avail parameter
 * @param name readable name of SSL library
 * @param avail NULL-terminated array of available backends
 * @return CURLsslset value
 */
CURLsslset curl_global_sslset(curl_sslbackend id, const char *name,
			      const curl_ssl_backend ***avail)
{
	if (name && strlen(name) < sizeof(ssl_backend))
		strlcpy(ssl_backend, name, sizeof(ssl_backend));

	lazy_load_curl();
	return curl_global_sslset_func(id, name, avail);
}

/**
 * Clean up global cURL state
 *
 * This function releases resources acquired by curl_global_init. It should
 * be called after all cURL handles have been cleaned up.
 */
void curl_global_cleanup(void)
{
	lazy_load_curl();
	curl_global_cleanup_func();
}

/**
 * Append a string to a curl slist.
 *
 * Appends a string to a linked list of strings held in a curl slist.
 * Returns a pointer to the start of the list.
 *
 * @param list Pointer to the start of an existing slist or NULL
 * @param data String to append to the list
 * @return Pointer to the start of the list
 */
struct curl_slist *curl_slist_append(struct curl_slist *list, const char *data)
{
	lazy_load_curl();
	return curl_slist_append_func(list, data);
}

/**
 * Free a curl slist.
 *
 * This function frees a curl slist and all of its associated
 * string data.
 *
 * @param list The curl slist to free.
 */
void curl_slist_free_all(struct curl_slist *list)
{
	lazy_load_curl();
	curl_slist_free_all_func(list);
}

/**
 * Get string description of the given error code.
 *
 * This function returns a human-readable string description for the given
 * CURLcode error value.
 *
 * @param error The error code to get the description for
 * @return A string describing the error code
 */
const char *curl_easy_strerror(CURLcode error)
{
	lazy_load_curl();
	return curl_easy_strerror_func(error);
}

/**
 * Initialize a new multi handle.
 *
 * This function initializes a new multi handle for curl. The returned
 * handle can be used to add individual curl handles that should be
 * handled together.
 *
 * @return New curl multi handle
 */
CURLM *curl_multi_init(void)
{
	lazy_load_curl();
	return curl_multi_init_func();
}

/**
 * Add a curl handle to a multi handle.
 *
 * This function adds a curl handle to a curl multi handle.
 *
 * @param multi_handle Curl multi handle
 * @param curl_handle Curl handle to add
 * @return CURLMcode return code
 */
CURLMcode curl_multi_add_handle(CURLM *multi_handle, CURL *curl_handle)
{
	lazy_load_curl();
	return curl_multi_add_handle_func(multi_handle, curl_handle);
}

/**
 * Remove a curl handle from a multi handle.
 *
 * This function removes a curl handle from a curl multi handle.
 *
 * @param multi_handle Curl multi handle
 * @param curl_handle Curl handle to remove
 * @return CURLMcode return code
 */
CURLMcode curl_multi_remove_handle(CURLM *multi_handle, CURL *curl_handle)
{
	lazy_load_curl();
	return curl_multi_remove_handle_func(multi_handle, curl_handle);
}

/**
 * Set file descriptor sets to watch.
 *
 * This function sets the file descriptor sets to be used with select()
 * or poll() to determine when to call curl_multi_perform().
 *
 * @param multi_handle Curl multi handle
 * @param read_fd_set File descriptor set for reading
 * @param write_fd_set File descriptor set for writing
 * @param exc_fd_set File descriptor set for exceptions
 * @param max_fd Maximum file descriptor value
 * @return CURLMcode return code
 */
CURLMcode curl_multi_fdset(CURLM *multi_handle, fd_set *read_fd_set,
			   fd_set *write_fd_set, fd_set *exc_fd_set,
			   int *max_fd)
{
	lazy_load_curl();
	return curl_multi_fdset_func(multi_handle, read_fd_set, write_fd_set, exc_fd_set, max_fd);
}

/**
 * Run curl handles in multi handle.
 *
 * This function handles transfers on all the added handles that need
 * attention in the multi handle. It calls the curl_perform function for
 * each handle. This is a non-blocking function that returns as soon as all
 * transfers have been dealt with. It must be called regularly for transfers
 * to occur.
 *
 * The running_handles parameter is unused.
 *
 * @param multi_handle Curl multi handle
 * @param running_handles Unused parameter
 * @return CURLMcode return code
 */
CURLMcode curl_multi_perform(CURLM *multi_handle, int *running_handles)
{
	lazy_load_curl();
	return curl_multi_perform_func(multi_handle, running_handles);
}

/**
 * Clean up multi handle.
 *
 * This closes all the connections and frees all resources used by
 * the multi handle. Call this when the program no longer needs to use
 * the multi handle. After this call, the CURLM handle is no longer valid.
 *
 * @param multi_handle Curl multi handle to clean up
 * @return CURLMcode return code
 */
CURLMcode curl_multi_cleanup(CURLM *multi_handle)
{
	lazy_load_curl();
	return curl_multi_cleanup_func(multi_handle);
}

/**
 * Read multi stack informationals.
 *
 * This function reads one or more messages from the multi handle information
 * stack. It returns a pointer to a curl_multi_info structure or NULL when no
 * more messages are available. The nummsgs parameter is passed in as a pointer
 * to an int. If it is non-NULL, the referenced int will be incremented by the
 * number of messages that were read from the stack.
 *
 * @param multi_handle Curl multi handle
 * @param msgs_in_queue Number of messages read (output)
 * @return Pointer to a curl_multi_info structure or NULL
 */
CURLMsg *curl_multi_info_read(CURLM *multi_handle, int *msgs_in_queue)
{
	lazy_load_curl();
	return curl_multi_info_read_func(multi_handle, msgs_in_queue);
}

/**
 * Return string describing curl multi error code.
 *
 * This returns a human readable string describing the given
 * CURLMcode value.
 *
 * @param error CURLMcode value to get string for
 * @return String describing error code
 */
const char *curl_multi_strerror(CURLMcode error)
{
	lazy_load_curl();
	return curl_multi_strerror_func(error);
}

/**
 * Set timeout for curl multi handle operations.
 *
 * This sets the maximum time in milliseconds that functions executing
 * on the given curl multi handle should allow to run before returning
 * control to the application. This only applies to the multi handle, not
 * to transfers running on easy handles that are part of this multi handle.
 *
 * @param multi_handle curl multi handle
 * @param milliseconds maximum time in milliseconds
 * @return CURLMcode return code
 */
CURLMcode curl_multi_timeout(CURLM *multi_handle, long *milliseconds)
{
	lazy_load_curl();
	return curl_multi_timeout_func(multi_handle, milliseconds);
}

/**
 * Initialize a curl easy session.
 * This function initializes a new curl easy handle for subsequent transfers.
 * It returns a CURL handle that should be passed to other curl_easy_*
 * functions. This handle should be cleaned up with curl_easy_cleanup when it is
 * no longer needed.
 */
CURL *curl_easy_init(void)
{
	lazy_load_curl();
	return curl_easy_init_func();
}

/**
 * Perform a curl easy transfer.
 * This function performs the previously configured curl easy transfer.
 * It should be called after curl_easy_setopt() has configured the transfer.
 *
 * @param curl Handle to a curl easy session.
 * @return A CURLcode indicating success or failure of the transfer.
 */
CURLcode curl_easy_perform(CURL *curl)
{
	lazy_load_curl();
	return curl_easy_perform_func(curl);
}

/**
 * Clean up a curl easy handle.
 * This function releases resources associated with the given curl easy handle.
 * It should be called when the handle is no longer needed.
 */
void curl_easy_cleanup(CURL *curl)
{
	lazy_load_curl();
	curl_easy_cleanup_func(curl);
}

/**
 * Duplicate a curl easy handle.
 *
 * This function duplicates the given curl easy handle, returning a new
 * handle with all options set exactly the same as the input handle.
 */
CURL *curl_easy_duphandle(CURL *curl)
{
	lazy_load_curl();
	return curl_easy_duphandle_func(curl);
}

#ifndef CURL_IGNORE_DEPRECATION
#define CURL_IGNORE_DEPRECATION(x) x
#endif

#ifndef CURLOPTTYPE_BLOB
#define CURLOPTTYPE_BLOB 40000
#endif

#undef curl_easy_getinfo
/**
 * Get information about a curl easy handle.
 *
 * This function retrieves information about the given curl easy handle.
 * The info parameter specifies what information to retrieve. Additional
 * varargs are used to return the requested information. For example,
 * info = CURLINFO_RESPONSE_CODE would return the response code for the
 * previous transfer in the supplied long *.
 *
 * Returns CURLE_OK on success.
 */
CURLcode curl_easy_getinfo(CURL *curl, CURLINFO info, ...)
{
	va_list ap;
	CURLcode res;

	va_start(ap, info);
	lazy_load_curl();
	CURL_IGNORE_DEPRECATION(
		if (info >= CURLINFO_LONG && info < CURLINFO_DOUBLE)
			res = curl_easy_getinfo_long_func(curl, info, va_arg(ap, long *));
		else if ((info >= CURLINFO_STRING && info < CURLINFO_LONG) ||
			 (info >= CURLINFO_SLIST && info < CURLINFO_SOCKET))
			res = curl_easy_getinfo_pointer_func(curl, info, va_arg(ap, void **));
		else if (info >= CURLINFO_OFF_T)
			res = curl_easy_getinfo_off_t_func(curl, info, va_arg(ap, curl_off_t *));
		else
			die("%s:%d: TODO (info: %d)!", __FILE__, __LINE__, info);
	)
	va_end(ap);
	return res;
}

#undef curl_easy_setopt
/**
 * Set options for a curl easy handle.
 *
 * This function sets options for the given curl easy handle. The options
 * are specified by the opt parameter and any additional varargs. Options
 * like CURLOPT_URL take a char* argument, while options like
 * CURLOPT_TIMEOUT_MS take a long argument.
 *
 * Returns CURLE_OK on success.
 */
CURLcode curl_easy_setopt(CURL *curl, CURLoption opt, ...)
{
	va_list ap;
	CURLcode res;

	va_start(ap, opt);
	lazy_load_curl();
	CURL_IGNORE_DEPRECATION(
		if (opt >= CURLOPTTYPE_LONG && opt < CURLOPTTYPE_OBJECTPOINT)
			res = curl_easy_setopt_long_func(curl, opt, va_arg(ap, long));
		else if (opt >= CURLOPTTYPE_OBJECTPOINT && opt < CURLOPTTYPE_OFF_T)
			res = curl_easy_setopt_pointer_func(curl, opt, va_arg(ap, void *));
		else if (opt >= CURLOPTTYPE_OFF_T && opt < CURLOPTTYPE_BLOB)
			res = curl_easy_setopt_off_t_func(curl, opt, va_arg(ap, curl_off_t));
		else
			die("%s:%d: TODO (opt: %d)!", __FILE__, __LINE__, opt);
	)
	va_end(ap);
	return res;
}
